// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:io' as io;

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart' show LocalPlatform, Platform;
import 'package:process/process.dart' show LocalProcessManager, ProcessManager;
import 'package:pub_semver/pub_semver.dart';

import 'data_types.dart';

/// An exception class to allow capture of exceptions generated by the Snippets
/// package.
class SnippetException implements Exception {
  SnippetException(this.message, {this.file, this.line});
  final String message;
  final String? file;
  final int? line;

  @override
  String toString() {
    if (file != null || line != null) {
      final String fileStr = file == null ? '' : '$file:';
      final String lineStr = line == null ? '' : '$line:';
      return '$runtimeType: $fileStr$lineStr: $message';
    } else {
      return '$runtimeType: $message';
    }
  }
}

/// Gets the number of whitespace characters at the beginning of a line.
int getIndent(String line) => line.length - line.trimLeft().length;

/// Contains information about the installed Flutter repo.
class FlutterInformation {
  FlutterInformation({
    this.platform = const LocalPlatform(),
    this.processManager = const LocalProcessManager(),
    this.filesystem = const LocalFileSystem(),
  });

  final Platform platform;
  final ProcessManager processManager;
  final FileSystem filesystem;

  static FlutterInformation? _instance;

  static FlutterInformation get instance => _instance ??= FlutterInformation();

  @visibleForTesting
  static set instance(FlutterInformation? value) => _instance = value;

  Directory getFlutterRoot() {
    if (platform.environment['FLUTTER_ROOT'] != null) {
      return filesystem.directory(platform.environment['FLUTTER_ROOT']);
    }
    return getFlutterInformation()['flutterRoot'] as Directory;
  }

  Version getFlutterVersion() =>
      getFlutterInformation()['frameworkVersion'] as Version;

  Version getDartSdkVersion() =>
      getFlutterInformation()['dartSdkVersion'] as Version;

  Map<String, dynamic>? _cachedFlutterInformation;

  Map<String, dynamic> getFlutterInformation() {
    if (_cachedFlutterInformation != null) {
      return _cachedFlutterInformation!;
    }

    String flutterVersionJson;
    if (platform.environment['FLUTTER_VERSION'] != null) {
      flutterVersionJson = platform.environment['FLUTTER_VERSION']!;
    } else {
      String flutterCommand;
      if (platform.environment['FLUTTER_ROOT'] != null) {
        flutterCommand = filesystem
            .directory(platform.environment['FLUTTER_ROOT'])
            .childDirectory('bin')
            .childFile('flutter')
            .absolute
            .path;
      } else {
        flutterCommand = 'flutter';
      }
      io.ProcessResult result;
      try {
        result = processManager.runSync(
            <String>[flutterCommand, '--version', '--machine'],
            stdoutEncoding: utf8);
      } on io.ProcessException catch (e) {
        throw SnippetException(
            'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e');
      }
      if (result.exitCode != 0) {
        throw SnippetException(
            'Unable to determine Flutter information, because of abnormal exit to flutter command.');
      }
      flutterVersionJson = (result.stdout as String).replaceAll(
          'Waiting for another flutter command to release the startup lock...',
          '');
    }

    final Map<String, dynamic> flutterVersion =
        json.decode(flutterVersionJson) as Map<String, dynamic>;
    if (flutterVersion['flutterRoot'] == null ||
        flutterVersion['frameworkVersion'] == null ||
        flutterVersion['dartSdkVersion'] == null) {
      throw SnippetException(
          'Flutter command output has unexpected format, unable to determine flutter root location.');
    }

    final Map<String, dynamic> info = <String, dynamic>{};
    info['flutterRoot'] =
        filesystem.directory(flutterVersion['flutterRoot']! as String);
    info['frameworkVersion'] =
        Version.parse(flutterVersion['frameworkVersion'] as String);

    final RegExpMatch? dartVersionRegex =
        RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?')
            .firstMatch(flutterVersion['dartSdkVersion'] as String);
    if (dartVersionRegex == null) {
      throw SnippetException(
          'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.');
    }
    info['dartSdkVersion'] = Version.parse(
        dartVersionRegex.namedGroup('detail') ??
            dartVersionRegex.namedGroup('base')!);
    _cachedFlutterInformation = info;

    return info;
  }
}

/// Injects the [injections] into the [template], while turning the
/// "description" injection into a comment.
String interpolateTemplate(
  List<SkeletonInjection> injections,
  String template,
  Map<String, Object?> metadata, {
  bool addCopyright = false,
}) {
  String wrapSectionMarker(Iterable<String> contents, {required String name}) {
    if (contents.join().trim().isEmpty) {
      // Skip empty sections.
      return '';
    }
    // We don't wrap some sections, because otherwise they generate invalid files.
    final String result = <String>[
      ...contents,
    ].join('\n');
    final RegExp wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true);
    return result.replaceAllMapped(
        wrappingNewlines, (Match match) => match.group(1)!);
  }

  return '${addCopyright ? '{{copyright}}\n\n' : ''}$template'
      .replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) {
    final String name = match[1]!;
    final int componentIndex = injections
        .indexWhere((SkeletonInjection injection) => injection.name == name);
    if (metadata[name] != null && componentIndex == -1) {
      // If the match isn't found in the injections, then just return the
      // metadata entry.
      return wrapSectionMarker((metadata[name]! as String).split('\n'),
          name: name);
    }
    return wrapSectionMarker(
        componentIndex >= 0
            ? injections[componentIndex].stringContents
            : <String>[],
        name: name);
  }).replaceAll(RegExp(r'\n\n+'), '\n\n');
}

class SampleStats {
  const SampleStats({
    this.totalSamples = 0,
    this.dartpadSamples = 0,
    this.snippetSamples = 0,
    this.applicationSamples = 0,
    this.wordCount = 0,
    this.lineCount = 0,
    this.linkCount = 0,
    this.description = '',
  });

  final int totalSamples;
  final int dartpadSamples;
  final int snippetSamples;
  final int applicationSamples;
  final int wordCount;
  final int lineCount;
  final int linkCount;
  final String description;
  bool get allOneKind =>
      totalSamples == snippetSamples ||
      totalSamples == applicationSamples ||
      totalSamples == dartpadSamples;

  @override
  String toString() {
    return description;
  }
}

Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) {
  return elements
          ?.expand<CodeSample>((SourceElement element) => element.samples) ??
      const <CodeSample>[];
}

SampleStats getSampleStats(SourceElement element) {
  if (element.comment.isEmpty) {
    return const SampleStats();
  }
  final int total = element.sampleCount;
  if (total == 0) {
    return const SampleStats();
  }
  final int dartpads = element.dartpadSampleCount;
  final int snippets = element.snippetCount;
  final int applications = element.applicationSampleCount;
  final String sampleCount = <String>[
    if (snippets > 0) '$snippets snippet${snippets != 1 ? 's' : ''}',
    if (applications > 0)
      '$applications application sample${applications != 1 ? 's' : ''}',
    if (dartpads > 0) '$dartpads dartpad sample${dartpads != 1 ? 's' : ''}'
  ].join(', ');
  final int wordCount = element.wordCount;
  final int lineCount = element.lineCount;
  final int linkCount = element.referenceCount;
  final String description = <String>[
    'Documentation has $wordCount ${wordCount == 1 ? 'word' : 'words'} on ',
    '$lineCount ${lineCount == 1 ? 'line' : 'lines'}',
    if (linkCount > 0 && element.hasSeeAlso) ', ',
    if (linkCount > 0 && !element.hasSeeAlso) ' and ',
    if (linkCount > 0)
      'refers to $linkCount other ${linkCount == 1 ? 'symbol' : 'symbols'}',
    if (linkCount > 0 && element.hasSeeAlso) ', and ',
    if (linkCount == 0 && element.hasSeeAlso) 'and ',
    if (element.hasSeeAlso) 'has a "See also:" section',
    '.',
  ].join();
  return SampleStats(
    totalSamples: total,
    dartpadSamples: dartpads,
    snippetSamples: snippets,
    applicationSamples: applications,
    wordCount: wordCount,
    lineCount: lineCount,
    linkCount: linkCount,
    description: 'Has $sampleCount. $description',
  );
}

/// Exit the app with a message to stderr.
/// Can be overridden by tests to avoid exits.
// ignore: prefer_function_declarations_over_variables
void Function(String message) errorExit = (String message) {
  io.stderr.writeln(message);
  io.exit(1);
};
